/*
 * This file is part of AudioStream.
 * Copyright (C) 2010 Justin Li and Jacob Wirth
 *
 *  AudioStream is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  AudioStream is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with AudioStream.  If not, see <http://www.gnu.org/licenses/>.
 *  
 */

package org.frustra.audiostream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.net.URLEncoder;

import org.frustra.audiostream.mp3.MP3Loader;
import org.frustra.audiostream.mp3.MP3Segment;
import org.frustra.audiostream.networking.StreamingSocket;
import org.frustra.audiostream.util.Base64;

/**
 * This is the main class of AudioStream, used to create connections to Icecast servers
 */
public abstract class AudioStreamConnection
{
	/**
	 * Stream name
	 */
	private String name = "Default Name";
	
	/**
	 * Stream genre
	 */
	private String genre = "No Genre";
	
	/**
	 * Stream description
	 */
	private String description = "No Description";
	
	/**
	 * Stream url
	 */
	private String url = "http://code.google.com/p/audiostream/";
	
	/**
	 * Stream username
	 */
	private String username = "source";
	
	/**
	 * Stream password
	 */
	private String password = "hackme";
	
	/**
	 * Stream mountpoint
	 */
	private String mountpoint = "listen";
	
	/**
	 * Stream useragent
	 */
	private final String useragent = "audiocast/1.1";
	
	/**
	 * Stream bitrate
	 */
	private final int bitrate = 128;
	
	/**
	 * Stream public flag
	 */
	private boolean isPublic = true;
	
	/**
	 * Hostname
	 */
	private String host;
	
	/**
	 * Host port
	 */
	private int port;
	
	/**
	 * Connection socket
	 */
	private StreamingSocket client = null;
	
	/**
	 * System time for last sent packet
	 */
	private long sentTime = 0;
	
	/**
	 * Amount of time to sleep between packets
	 */
	private long sleepTime = 0;
	
	/**
	 * Number of consecutive errors
	 */
	private int errorCount = 0;

	/**
	 * MetaData info
	 */
	private String metaDataCache = "Unknown";

	/**
	 * MetaData timer
	 */
	private long metaTime = 0;
	
	/**
	 * Creates a new connection to the streaming server
	 * 
	 * @param host The url of the server to connect to
	 * @param port The port to connect on
	 * @param mountpoint The Icecast mount point to use for the connection
	 */
	public AudioStreamConnection(String host, int port, String mountpoint)
	{
		this.host = host;
		this.port = port;
		this.mountpoint = mountpoint;
	}
	
	
	/**
	 * Connects the socket and sends the HTTP SOURCE request
	 * 
	 * @return Whether the connect was successful or not
	 * @throws IOException
	 */
	public boolean connect() throws IOException
	{
		client = new StreamingSocket(host, port);
		client.println("SOURCE /" + mountpoint + " HTTP/1.0");
		client.println("Authorization: Basic " + Base64.encode(username+":"+password));
		client.println("ice-bitrate: " + bitrate);
		client.println("ice-name: " + name);
		client.println("ice-genre: " + genre);
		client.println("ice-description: " + description);
		client.println("ice-url: " + url);
		client.println("ice-public: " + (isPublic ? "1" : "0"));
		client.println("User-Agent: " + useragent);
		client.println("Content-Type: audio/mpeg");
		client.println("");
		
		while (client.hasInput()) {
			String line = client.readLine();
			if (line.equalsIgnoreCase("http/1.0 200 ok")) {
				System.out.println("Connection to Server Successful: " + line);
				return true;
			} else {
				System.out.println("Response: " + line + " --- NOT OK");
				return false;
			}
		}
		
		return false;
	}
	
	/**
	 * Updates the metadata cache to be sent to the server
	 * 
	 * @param song The name of the song that is playing
	 * @throws IOException
	 */
	private void updateMetaData(String song) throws IOException {
		metaDataCache = song;
		metaTime = 0;
	}
	
	/**
	 * Causes the program to sync with the MP3 stream (ie wait the correct amount of time until sending the next packet)
	 */
	private void sync() {
		if (sleepTime > 0 ) {
			try {
				Thread.sleep(sleepTime);
			} catch (Exception e) {
				return;
			}
			sleepTime += sentTime - System.currentTimeMillis();
		}
	}
	
	/**
	 * Sends meta data updates to the server when required
	 * @throws IOException
	 */
	private void syncMetaData() throws IOException {
		long sleep = metaTime - System.currentTimeMillis();
		if (sleep <= 0) {
			Socket page = new Socket(host, port);
			PrintStream stream = new PrintStream(page.getOutputStream());
			stream.println("GET /admin/metadata.xsl?song=" + URLEncoder.encode(metaDataCache, "UTF-8") + "&mount=" + URLEncoder.encode("/" + mountpoint, "UTF-8") + "&mode=updinfo&charset=UTF-8 HTTP/1.0");
			stream.println("Authorization: Basic " + Base64.encode(username+":"+password));
			stream.println("User-Agent: " + useragent);
			stream.println();
			metaTime = System.currentTimeMillis() + 30000;
		}
	}
	
	/**
	 * Sends streaming packets to the server
	 * 
	 * @param s The segment to be sent to the server
	 * @throws IOException
	 */
	private void send(MP3Segment s) throws IOException
	{
		client.print(s.getData());
		client.flush();
		sentTime = System.currentTimeMillis();
		sleepTime += s.getTime();
	}
	
	/**
	 * This method must be overridden to specify the playlist
	 * 
	 * @return The filename of the next song
	 * @throws Exception
	 */
	public abstract String getNextSong() throws Exception;

	/**
	 * Starts the MP3 stream to the server
	 * Note, it is synchronous, so after calling this the program will commence and not end until it is closed or encounters an exception
	 * 
	 * @throws InterruptedException
	 * @throws IOException
	 */
	public void startStream() throws InterruptedException, IOException
	{
		try {
			MP3Loader song = null;
			while (true)
			{
				if (song == null || song.isFinished())
				{
					String fname = null;
					try
					{
						fname = getNextSong();
						song = new MP3Loader(new File(fname));
						updateMetaData(song.id3Data.get("TITLE") + " - " + song.id3Data.get("ARTIST"));
						System.out.println("Playing next song: " + metaDataCache);
						errorCount = 0;
					}
					catch (Exception e)
					{
						System.out.println("Could not load next song: " + fname);
						e.printStackTrace();
						errorCount++;
						if (errorCount >= 10)
						{
							System.out.println("Exceeded max consecutive errors (" + errorCount + ")");
							return;
						}
						continue;
					}
				}
				MP3Segment s = song.next();
				if (s != null)
				{
					send(s);
					syncMetaData();
					sync();
				}
			}
		} finally {
			client.close();
		}
	}
	
	/**
	 * Sets the stream description
	 * @param description
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * Sets the stream genre
	 * @param genre
	 */
	public void setGenre(String genre) {
		this.genre = genre;
	}

	/**
	 * Sets if the stream is public
	 * @param isPublic
	 */
	public void setPublic(boolean isPublic) {
		this.isPublic = isPublic;
	}

	/**
	 * Sets the stream's mountpoint
	 * @param mount
	 */
	public void setMount(String mount) {
		this.mountpoint = mount;
	}

	/**
	 * Sets the stream name
	 * @param name
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Sets the stream password
	 * @param password
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * Sets the stream username
	 * @param username
	 */
	public void setUsername(String username) {
		this.username = username;
	}

	/**
	 * Sets the stream url
	 * @param url
	 */
	public void setURL(String url) {
		this.url = url;
	}
}
